<?php
/*
 * Copyright (c) 2008 Sergey Bondari, bondari@bondari.net
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

namespace Mosaic\System;


/**
 * System node respresentation.
 *
 * This class is the central node control class often referred in papers as "kernel".
 * This class is always awailable from any place in code via global reference variable $sys.
 * It holds references to the object broker, request broker, component broker, shared context 
 * instance, ICC component etc. It also provide reference to special components instances 
 * called kernel modules (as $sys->kernelmodulename).
 * 
 *
 * @package System
 * @subpackage Node
 */
class Node {
	
	/**
	 * Unique text identifier of a node
	 * @var string
	 */
	private $name;

	/**
	 * System configuration
	 * @var Config
	 */
	private $config;

	/**
	 * System error handler
	 * @var Debug
	 */
	private $debug;

	/**
	 * RDF parser
	 * @var aclRDF
	 */
	private $rdf;

	/**
	 * System ICC
	 * @var ICC
	 */
	private $icc;
	
	/**
	 * System component broker
	 * @var ComponentBroker
	 */
	private $componentBroker;

	/**
	 * System request broker
	 * @var RequestBroker
	 */
	private $requestBroker;

	/**
	 * System object broker
	 * @var ObjectBroker
	 */
	private $objectBroker;

	/**
	 * Execution context
	 * @var Context
	 */
	private $context;

	/**
	 * Default execution context.
	 *
	 * When node does not run in the request context, this variable stores
	 * reference to the default context object. In analogy with OS, this 
	 * might be taken as the kernel context.
	 * @var Context
	 */
	private $defaultContext;


	/**
	 * Active contexts
	 *
	 * Stores all active context instances between switches of contexts. 
	 * References are removed upon context release operation.
	 * @var array Hash array of Context
	 */
	private $contextPool;

	/**
	 * PID of worker proces
	 * @var integer
	 */
	private $wtPID;

	/**
	 * PID of object broker process
	 * @var integer
	 */
	private $obPID;

	/**
	 * PID of request broker process
	 * @var integer
	 */
	private $rbPID;
	
	/**
	 * Socket to worker process
	 * @var stream
	 */
	private $wtSocket;

	/**
	 * Socket to object broker process
	 * @var stream
	 */
	private $obSocket;

	/**
	 * Query socket to naming facility
	 * @var stream
	 */
	private $nsSocket;


	/**
	 * Socket to control agent facility
	 * @var stream
	 */
	private $nsMsgSocket;


	/**
	 * Current process type
	 * @var PRECURSOR|WORKER|REQUEST_BROKER|OBJECT_BROKER
	 */
	private $role;



	/**
	 * Node constructor.
	 *
	 * @param Config
	 */
	function __construct($config) {
		$this->config = $config;
		$this->name = $config->get('node.name');
		$this->role = PRECURSOR;
	}

	/**
	 * Node entry point
	 *
	 * @return void
	 */
	public function start() {
		/*
		 * Init debug handlers
		 */
		$this->initDebug();

		trigger_error('+System starting.');

		/*
		 * Init ICC subsystem
		 */
		$this->initICC();

		trigger_error('ICC kernel module initialized.');

		/*
		 * Parse and preload all packages information
		 */
		$this->initComponentBroker();

		trigger_error('Packages kernel module initialized.');

		/*
		 * Connect to control agent and announce node up manifest
		 */
		$this->nodeUpManifest();
		trigger_error('Node up.');

		trigger_error('-Creating threads.');

		/*
		 * Create worker, object broker and request broker
		 * Function also presets Node::$role variable to correct value
		 */
		$this->createThreads();



		switch ($this->role) {
			case WORKER:
				trigger_error('Worker process reporting.');
				/*
				 * Creating request broker connector
				 */
				$this->initRequestBroker();

				/*
				 * Wait 0.5 seconds for other daemons to initialize
				 */
				usleep(500000);

	
				/*
				 * Switch to default context
				 */
				$this->switchDefaultContext();	

				/*
				 * Entering process request loop
				 */
				$this->requestBroker->processRequests();

				trigger_error('Node is terminating ...');
			break;

			case REQUEST_BROKER:
				trigger_error('-Request broker process reporting.');
				$this->initRequestBroker();
				trigger_error('Request broker has exited.', E_USER_WARNING);
			break;

			case OBJECT_BROKER:
				trigger_error('-Object broker process reporting.');
				$this->initRequestBroker();
				trigger_error('Request broker kernel module initialized.');
				$this->initObjectBroker();
				trigger_error('Object broker kernel module initialized.');

				/*
				 * Wait 0.25 seconds for other daemons to initialize
				 */
				usleep(250000);

				/*
				 * Switch to default context
				 */
				$this->switchDefaultContext();	

				$this->requestBroker->processRequests();
			break;

		}

	}

	/**
	 * Debug handlers initialization
	 * @return void
	 */
	private function initDebug() {
		$this->debug = new Debug();
	}

	/**
	 * Inter component system initalization
	 * @return void
	 */
	private function initICC() {
		$this->icc = new ICC();
	}

	/**
	 * Request broker initialization
	 *
	 * Worker and object broker get RequestMultiplexor instance. Request broker gets RequestBroker instance.
	 * @return void
	 */
	private function initRequestBroker() {
		if ($this->role == \Mosaic\System\REQUEST_BROKER) {
			$this->requestBroker = new RequestBroker();
			$this->requestBroker->run();
			/*
			 * Never reaches this point. Start is the request broker service loop.
			 */
		} else {
			$this->requestBroker = new RequestMultiplexor();
		}

	}

	/**
	 * Object broker initalization
	 * @return void
	 */
	private function initObjectBroker() {
		$this->objectBroker = new ObjectBroker();
		$this->objectBroker->start();
	}

	
	/**
	 * Component broker initalization
	 * @return void
	 */
	private function initComponentBroker() {
		$this->componentBroker = new ComponentBroker();
		$this->componentBroker->init();
	}
	


	/**
	 * Switch to default context
	 *
	 * Replaces current context with default context. If default context was not instantiated
	 * method instantiates it.
	 *
	 * @return void
	 */
	public function switchDefaultContext() {
		trigger_error('Switching to default context');
		$this->context = false; return;
		if (!$this->defaultContext instanceof Context) {
			$this->defaultContext = new Context();
		}
		$this->context = $this->defaultContext;
	}


	/**
	 * Switch worker process context
	 *
	 * Synchronize node execution context with given context tag.
	 * If context tag is not provided or empty, method creates a new context
	 * and returns a context tag. Context tag is the Context obejct URI.
	 *
	 * @param string Context tag (URI of remote context object).
	 * @return string Context tag
	 */
	public function switchContext($tag = NULL) {
		trigger_error('Switching to '.($tag?$tag:'new context'));

		
		if (!$tag) {
			/*
			 * Creation of a new context
			 */
			trigger_error('Creating a new context.');
			$context = new Context();
			$tag = $context->__getUri();
			$this->contextPool[$tag] = array($context,1);
			$this->context = $context;
		} else {
			trigger_error('Binding to context '.$tag);
			/*
			 * Synchronization with existing context. First checks in local pool of contexts,
			 * if not there, instantiate and bind
			 */
			if (isset($this->contextPool[$tag])) {
				$this->context = $this->contextPool[$tag][0];
			} else {
				$context = new Context();
				try {
					/*
					 * Explicit binding
					 */
					$context->__bindUri($tag);
					$this->contextPool[$tag] = array($context,0);
					$this->context = $context;
				} catch (RemoteExceptionThrowable $e) {
					trigger_error('Error while binding to context '.$tag, E_USER_ERROR);
					$this->switchDefaultContext();
					$tag = false;
				}
			}
		}
		trigger_error('Context switched to '.$tag);
		return $tag;
	}

	/**
	 * Release node context
	 *
	 * Releases a node context. Context is switched to default context.
	 *
	 * @param $tag context tag
	 * @return void
	 */
	public function releaseContext($tag) {
		global $sys;
		trigger_error('Releasing local stateful context data of '.$tag);
		if (!isset($this->contextPool[$tag])) {
			trigger_error('Context '.$tag.' is unknown to me');
		} else {
			/*
			 * Free stateful component instances
			 */
			if ($sys->role == WORKER) $this->componentBroker->releaseContext($tag);
	
			/*
			 * Release stateful object broker resources
			 */
			if ($sys->role == OBJECT_BROKER) $this->objectBroker->releaseContext($tag);

			if ($this->context && $this->context->__getUri() == $tag) {
				$this->switchDefaultContext();
			}


			/*
			 * This node was a context initiator
			 * means it has to broadcast a context release message
			 */
			if ($this->contextPool[$tag][1]) {
				$_r = new ReleaseContextRequest();
				$_r->setContextTag($tag);
				$_r->execAsync();
			}

			/*
			 * Release context from pool of known contexts
			 */
			unset($this->contextPool[$tag]);
			
		}
	}


	/**
	 * Get current context tag.
	 *
	 * Actually returns current context stub URI
	 * @return void
	 */
	public function getContextTag() {
		if ($this->context) return $this->context->__getUri();
	}

	/**
	 * Create node processes
	 * Creates a worker, request broker and object broker processes by forking initial process.
	 * Method also establishes IPC UNIX sockets between processes.
	 *
	 * @return void
	 */
	private function createThreads() {
		/*
		 * Create WORKER <-> REQUEST BROKER socket pair
		 */
		$_rbSockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

		/*
		 * Create OBJECT BROKER <-> REQUEST BROKER socket pair
		 */
		$_obSockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);

		if (!$_rbSockets || !$_obSockets) {
			throw new RuntimeException('Could not create unix sockets.');
		}

		/*
		 * Current process becomes worker
		 */
		$this->wtPID = posix_getpid();


		/*
		 * Create a Request Broker process
		 */
		$pid = pcntl_fork();


		switch ($pid) {
			case 0: // Request Broker
				$this->role = REQUEST_BROKER;
				$this->rbPID = posix_getpid();
				fclose($_rbSockets[0]);
				fclose($_obSockets[0]);
				$this->wtSocket = $_rbSockets[1];
				$this->obSocket = $_obSockets[1];
			break;
			case -1:
				throw new \Exception('ICC could not fork()');
			break;

			default: // Worker/Object broker
				$this->rbPID = $pid;
				$pid = pcntl_fork();
				fclose($this->nsMsgSocket);
				$this->nsMsgSocket = NULL;
				switch ($pid) {
					case 0: // Object broker
						$this->role = OBJECT_BROKER;
						$this->obPID = posix_getpid();
						fclose($_rbSockets[0]);
						fclose($_rbSockets[1]);
						fclose($_obSockets[1]);
						$this->rbSocket = $_obSockets[0];

					break;
					case -1:
						throw new \Exception('ICC could not fork()');
					break;
					default: // Worker
						$thiks->obPID = $pid;
						$this->role = WORKER;
						$this->rbSocket = $_rbSockets[0];
						fclose($_obSockets[0]);
						fclose($_obSockets[1]);
						fclose($_rbSockets[1]);
						
				}


				
		}
		
		
	}

	/**
	 * Function establishes connection with nameserver,
	 * registers node with nameserver and tests the registration
	 * @return void
	 */
	private function nodeUpManifest() {
		global $sys;

		/*
		 * Establish connection to control agent
		 */
		trigger_error('+Registering node with NS');
		$this->nsMsgSocket = stream_socket_client($sys->config->get('node.nameserver'),$errno,$errstr);

		if (!$this->nsMsgSocket) {
			throw new IOException('Could not connect to NS at '.$sys->config->get('node.nameserver'));
		}

		/*
		 * Send node up manifest
		 */
		$req = new NodeUpManifest($sys->config->get('node.name'), $sys->config->get('node.dsn'), $this->componentBroker->localPackages->getManifest());

		$response = $req->sendReceive($this->nsMsgSocket);
		$response->getData();
		

		/* 
		 * Establish connection to naming facility
		 */
		trigger_error('Asking nameserver for my socket address');
		$this->nsSocket = stream_socket_client($sys->config->get('node.nameserver'),$errno,$errstr);

		if (!$this->nsSocket) {
			throw new IOException('Could not connect to NS at '.$sys->config->get('node.nameserver'));
		}

		/* 
		 * Verify successful registration of the node.
		 */
	 	$req = new NodeResolveRequest($sys->config->get('node.name'));
		$response = $req->sendReceive($this->nsSocket);
		if ($response->getData() != $sys->config->get('node.dsn')) {
			throw new RuntimeException('NS malfunction. Node registration failed.');
		}
		trigger_error('-Socket address matches. NS registration successfull.');
	}

	/**
	 * Node termination routine
	 *
	 * Function shuts down connection with nameserver,
	 * and unregisters node with nameserver.
	 * @return void
	 */
	private function nodeDownManifest() {
		global $sys;
		
		if (!$this->nsSocket) {
			trigger_error('NS query connection not established.');
		} else {
			fclose($this->nsSocket);
			$this->nsSocket = NULL;
		}

		if (!$this->nsMsgSocket) {
			trigger_error('NS control connection not established.');
		} else {
			$req = new NodeDownManifest($this->name);
			trigger_error("Sending node down manifest ...");
			$req->sendReceive($this->nsMsgSocket);
			trigger_error('NS unregistration successfull.');
			fclose($this->nsMsgSocket);
			$this->nsMsgSocket = null;
		}
		
	}




	/**
	 * Property wrapper
	 *
	 * Private property wrapper to avoid property damage from outside.
	 * Method also plays a substantial role in kernel modules functionality.
	 * When trying to access kernel module instance, object broker creates or substitues
	 * correct component instance according to the current context.
	 * 
	 * @param string Property name
	 * @return mixed
	 */
	function __get($key) {
		if (isset($this->{$key})) {
			return $this->{$key}; 
		} else {
			try {
				return $this->componentBroker->getKModuleInstance($key);
			} catch (KernelException $e) {
				throw new RuntimeException($key . ' property is not accessible on the node instance.');
			}
		}
	}



	/**
	 * Node destructor
	 *
	 * Sends node shutdown, shuts down other processes of node.
	 */
	function __destruct() {

		/*
		 * This piece of code closes all node ICC sockets
		 */
		if ($this->wtSocket) fclose($this->wtSocket);
		if ($this->obSocket) fclose($this->obSocket);
		if ($this->rbSocket) fclose($this->rbSocket);

		/*
		 * Gracefully close connection to NS
		 */
		$this->nodeDownManifest();
	
		/*
		 * Working thread must terminate request broker and object broker
		 */
		if ($this->role == WORKER) {
			trigger_error('Terminating request broker process.');
			if (posix_kill($this->rbPID, SIGTERM)) {
				trigger_error('Waiting for request broker termination.');
				pcntl_waitpid($this->rbPID, $status);
				trigger_error('Request broker terminated.');
			}

			trigger_error('Terminating object broker.');
			if (posix_kill($this->obPID, SIGTERM)) {
				trigger_error('Waiting for object broker termination.');
				pcntl_waitpid($this->obPID, $status);
				trigger_error('Object broker terminated.');
			}
		}


	}



}

